🚗 TimiAuto en México
🛡️ RC NorthboundAuto mexicano en USA
✈️ Tourist AutoPlacas USA en México
Cómo funciona Aseguradoras
🇺🇸 🇲🇽 Driving to Mexico? Get insured in seconds
En línea ahora

Tu seguro
de auto en segundos

Sin formularios. Sin llamadas. Solo platica con Timi y obtén la mejor cotización de +8 aseguradoras al instante.

Cómo funciona ↓
+150cotizaciones servidas
·
7aseguradoras
·
⏱ 30scotización al instante
Respaldados por Backed by Chubb Quálitas
T

Timi

● En línea
¡Hola! 👋 Soy Timi. Dime qué auto quieres asegurar.
Quiero asegurar mi KIA K3 2026
¡Listo! Encontré tu KIA K3 2026. Cotizando con 8 aseguradoras...
⚡ Mejores precios — Amplia
🥇 Chubb$8,420.00
🥈 Qualitas$9,150.00
🥉 HDI$9,780.00
+5 opciones másVer todas →
Toca para cotizar tu auto →

Cotizamos con las mejores aseguradoras de México

Tres pasos. Cero estrés.

Olvídate de formularios interminables. Con Timi cotizas como si le mandaras un mensaje a un amigo.

💬
01

Platica

Escribe qué auto quieres asegurar. "Mi Jetta 2024" o "Mazda CX-5 nueva". Timi entiende lenguaje natural.

02

Compara

En segundos recibe cotizaciones de +8 aseguradoras. Ve coberturas, precios y deducibles lado a lado.

🛡️
03

Contrata

Elige la que más te conviene. Te conectamos con un asesor para finalizar la contratación en minutos.

Seguros sin complicaciones

Diseñamos Timi para que asegurar tu auto sea tan fácil como pedir un Uber.

🤖

IA que entiende

No necesitas saber de seguros. Solo dile qué auto tienes y Timi hace el resto. Incluso puedes subir tu factura.

💰

Los mejores precios

Comparamos con las principales aseguradoras de México en tiempo real para darte el precio más competitivo.

📄

PDF y comparativo

Recibe tu cotización en un PDF profesional con detalle de coberturas, o compártela por WhatsApp.

🔒

Experiencia que respalda

Timi está respaldado por un despacho con años de experiencia en el mercado asegurador mexicano.

Cotiza tu seguro
ahora mismo

Es gratis, toma menos de un minuto y no necesitas registrarte.

T

Timi

● En línea
1Tu auto
2Opciones
3Contratar
al final del body. function hydrateReviews() { try { var reviews = (typeof window !== 'undefined' && Array.isArray(window.TIMI_REVIEWS)) ? window.TIMI_REVIEWS : []; var section = document.getElementById('reviewsSection'); var grid = document.getElementById('reviewsGrid'); if (!section || !grid) return; if (!reviews.length) { section.hidden = true; return; } grid.innerHTML = reviews.slice(0, 6).map(function(r){ var stars = '★'.repeat(Math.max(1, Math.min(5, r.rating || 5))) + '☆'.repeat(5 - Math.max(1, Math.min(5, r.rating || 5))); var safeText = String(r.text || '').replace(/[<>]/g,''); var safeName = String(r.name || 'Cliente').replace(/[<>]/g,''); var safeCity = String(r.city || '').replace(/[<>]/g,''); var safeDate = String(r.date || '').replace(/[<>]/g,''); return '
' + '
' + stars + '
' + '
"' + safeText + '"
' + '
' + safeName + (safeCity ? ' · ' + safeCity : '') + '' + safeDate + '
' + '
'; }).join(''); section.hidden = false; } catch(_) {} } // HERO_STATS_HYDRATE_v1 (2026-05-08): hidrata #statQuotes con cotizaciones_total reales de /api/stats function hydrateHeroStats() { try { fetch('/api/stats').then(function(r){return r.json();}).then(function(d){ var n = d && d.total && d.total.cotizaciones_total; if (!n || n < 100) return; // mantén el fallback "+150" si la BD aún no tiene volumen var rounded = Math.floor(n / 50) * 50; // redondea a múltiplos de 50 hacia abajo var el = document.getElementById('statQuotes'); if (el) el.textContent = '+' + rounded.toLocaleString('es-MX'); }).catch(function(){}); } catch(_) {} } // Auto-detect on load document.addEventListener('DOMContentLoaded', function() { hydrateHeroStats(); hydrateReviews(); var isTourist = location.pathname === '/tourist' || new URLSearchParams(location.search).get('mode') === 'tourist'; var isRC = location.pathname === '/rc' || new URLSearchParams(location.search).get('mode') === 'rc'; if (isTourist) { document.body.classList.add('tourist-mode'); document.body.dataset.mode = 'tourist'; // Fix 2026-04-20 (Michel): Tourist siempre en inglés por default para coincidir con ads Southbound EN // Evita discontinuidad con Google Ads y mejora Quality Score currentLang = 'en'; // Actualizar HTML lang, title, meta description y og tags document.documentElement.lang = 'en'; document.title = 'Mexico Car Insurance in Seconds | Timi'; var metaDesc = document.querySelector('meta[name="description"]'); if (metaDesc) metaDesc.setAttribute('content', 'No forms. No personal data. Just chat with Timi and get instant quotes from top insurers like Chubb. Buy your Mexico car insurance in minutes.'); var ogTitle = document.querySelector('meta[property="og:title"]'); if (ogTitle) ogTitle.setAttribute('content', 'Mexico Car Insurance in Seconds | Timi'); var ogDesc = document.querySelector('meta[property="og:description"]'); if (ogDesc) ogDesc.setAttribute('content', 'Get Mexico car insurance in seconds. No forms, no personal data needed. Just chat with Timi.'); applyTouristTexts(); } if (isRC) { document.body.classList.add('rc-mode'); document.body.dataset.mode = 'rc'; currentLang = 'es'; applyRCTexts(); } // Activate correct product switcher tab var psTab = isTourist ? 'ps-tourist' : isRC ? 'ps-rc' : 'ps-timi'; var psClass = isTourist ? 'active-tourist' : isRC ? 'active-rc' : 'active-timi'; var el = document.getElementById(psTab); if (el) el.classList.add(psClass); }); // ═══ Socket.IO ═══ function initSocket() { // Detect tourist mode from URL const timiMode = (location.pathname === '/tourist' || new URLSearchParams(location.search).get('mode') === 'tourist') ? 'tourist' : (location.pathname === '/rc' || new URLSearchParams(location.search).get('mode') === 'rc') ? 'rc' : 'auto'; document.body.dataset.mode = timiMode; if (timiMode === 'tourist') document.body.classList.add('tourist-mode'); if (timiMode === 'rc') document.body.classList.add('rc-mode'); // ═══ Socket.IO init — hardened para Android mobile networks (2026-04-23) ═══ // Polling primero luego upgrade a WS: más robusto en 4G/LTE Android + proxies corporativos. // Reconexión infinita con backoff para aguantar handoffs WiFi↔celular. socket = io({ transports: ['polling', 'websocket'], upgrade: true, timeout: 20000, reconnection: true, reconnectionAttempts: Infinity, reconnectionDelay: 1000, reconnectionDelayMax: 5000, query: { mode: timiMode, landing: (window.__TIMI_LANDING__ && window.__TIMI_LANDING__.slug) || '', landing_type: (window.__TIMI_LANDING__ && window.__TIMI_LANDING__.type) || '' } /* LANDING_CAPTURE_v1 */ }); // Diagnóstico de conexión (revisar console en Android si el gap de 92.5% drop-off en `inicio` persiste) try { socket.on('connect', () => { try { console.log('[timi-socket] conectado vía', socket.io.engine.transport.name); } catch(_){} }); socket.on('connect_error', (err) => { try { console.warn('[timi-socket] connect_error:', err && err.message); } catch(_){} }); socket.io.on('upgrade', (transport) => { try { console.log('[timi-socket] upgrade a', transport && transport.name); } catch(_){} }); socket.io.on('reconnect_attempt', (n) => { try { console.log('[timi-socket] reconnect_attempt #', n); } catch(_){} }); } catch(_){} // Enviar timezone del cliente al servidor socket.on('connect', () => { socket.emit('client-timezone', { offset: -new Date().getTimezoneOffset(), // minutos al este de UTC timezone: Intl.DateTimeFormat().resolvedOptions().timeZone }); }); // ═══ STRIPE CHECKOUT HANDLER ═══ socket.on('timi-stripe-checkout', async (data) => { try { const res = await fetch('/api/stripe/checkout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); const result = await res.json(); if (result.ok && result.url) { // Open Stripe in new window — keep chat alive for webhook response const stripeWin = window.open(result.url, '_blank'); if (!stripeWin) { appendMsg('bot', '💳 Click aquí para pagar\n\n⚠️ No cierres esta ventana. Tu póliza se emitirá automáticamente.'); } else { appendMsg('bot', '💳 Se abrió la ventana de pago.\n\n⚠️ **No cierres esta ventana.** Regresa aquí después de pagar — tu póliza aparecerá automáticamente.'); } // Poll for payment confirmation (backup if webhook is slow) let pollCount = 0; const pollInterval = setInterval(async () => { pollCount++; if (pollCount > 60) { clearInterval(pollInterval); return; } // 5 min max try { const statusRes = await fetch('/api/stripe/status/' + result.checkoutSessionId); const statusData = await statusRes.json(); if (statusData.paid) { clearInterval(pollInterval); // Notify server to check payment socket.emit('user-msg', { text: '__PAYMENT_CHECK__' }); } } catch(e) {} }, 5000); } else { appendMsg('bot', '❌ Error al crear sesión de pago: ' + (result.error || 'Unknown')); } } catch (e) { appendMsg('bot', '❌ Error de conexión con el procesador de pago.'); } }); // Clean up any payment URL params if (window.location.search.includes('payment')) { window.history.replaceState({}, '', window.location.pathname); } // Agente 9 — Auto-emitir CONFIRMAR_CONTRATACION tras clic en link de verificación socket.on('timi-emit-now', () => { try { addMessage('✅ Identidad verificada — emitiendo póliza…', 'user'); } catch(e) {} socket.emit('user-msg', { text: 'CONFIRMAR_CONTRATACION' }); }); socket.on('timi-msg', (data) => { // Remove typing indicator if present removeTyping(); if (data.format === 'typing') { showTyping(data.text); return; } if (data.format === 'quote-cards') { renderQuoteCards(data); setChatStep('opciones'); // STEP_CHIPS_v1 window.dataLayer = window.dataLayer || []; // META_CAPI_v1: eventID = mismo event_id del server para dedup en GTM Meta tag window.dataLayer.push({ 'event': 'quote_received', eventID: data._capiEventId || null }); return; } if (data.format === 'closing-card') { renderClosingCard(data); return; } if (data.format === 'factura-card') { renderFacturaCard(data.data); return; } if (data.format === 'factura-buttons') { // Botones de confirmación estilo FiTi const wrap = document.createElement('div'); wrap.className = 'version-buttons'; wrap.innerHTML = ` `; chatMessages.appendChild(wrap); scrollToBottom(); return; } if (data.format === 'emision-exitosa') { window.dataLayer = window.dataLayer || []; // Extraer monto numérico de "prima" (formato "$8,743.88" → 8743.88) var primaNum = 0; if (data.prima) { primaNum = parseFloat(String(data.prima).replace(/[^0-9.]/g, '')) || 0; } window.dataLayer.push({ 'event': 'purchase_completed', // META_CAPI_v1: eventID estable purchase_ para dedup pixel↔server eventID: data.poliza ? ('purchase_' + data.poliza) : null, 'value': primaNum, 'currency': 'MXN', 'product': 'auto_mx', 'insurer': data.cia || '' }); const wrap = document.createElement('div'); wrap.style.cssText = 'background:linear-gradient(135deg,#0d3320,#1a4a30);border-radius:16px;padding:20px;margin:8px 0;border:1px solid #00D4AA;'; wrap.innerHTML = `
🎉
¡PÓLIZA EMITIDA!
Póliza
${data.poliza}
🛡️ ${data.cia}
💰 ${data.prima}
🚗 ${data.vehiculo}
👤 ${data.cliente}
📧 ${data.email}
${data.pdfUrl ? ` 📄 Póliza ${data.cia||''} ${data.poliza} ` : ''} ${Array.isArray(data.opciones_pago) && data.opciones_pago.length ? `
⚠️ Debes pagar dentro de ${(data.pago_info?.dias_gracia)||3} días
${data.pago_info?.limite ? `
Fecha límite: ${data.pago_info.limite}
` : ''}
${data.opciones_pago.map(op => op.url ? `${op.label}` : `` ).join('')}
` : ''}
¡Gracias por confiar en Timi! 🚀
`; chatMessages.appendChild(wrap); scrollToBottom(); return; } if (data.format === 'gender-buttons') { addMessage(data.text || '¿Cuál es tu género?', 'bot'); const wrap = document.createElement('div'); wrap.style.cssText = 'display:flex;gap:12px;padding:8px 0;justify-content:center;'; wrap.innerHTML = ` `; chatMessages.appendChild(wrap); scrollToBottom(); return; } if (data.format === 'contratacion-card') { renderContratacionCard(data.data); return; } if (data.format === 'contratacion-confirm-buttons') { addMessage(data.text || '¿Todo correcto?', 'bot'); const wrap = document.createElement('div'); wrap.style.cssText = 'display:flex;gap:10px;padding:8px 0;justify-content:center;'; wrap.innerHTML = ` `; chatMessages.appendChild(wrap); scrollToBottom(); return; } if (data.format === 'redirect-suggestion') { const wrap = document.createElement('div'); wrap.style.cssText = 'align-self:flex-start;width:95%;animation:msgSlide 0.3s ease;margin:4px 0'; let html = '
'; html += '
👋 Parece que necesitas un seguro diferente. Te llevo al chat correcto:
'; html += '
'; if (data.needsRC) { html += ''; } if (data.needsTourist) { html += ''; } html += ''; html += '
'; wrap.innerHTML = html; chatMessages.appendChild(wrap); scrollToBottom(); return; } if (data.format === 'product-selector') { const wrap = document.createElement('div'); wrap.className = 'ps-card'; wrap.style.cssText = 'align-self:flex-start;width:95%;animation:msgSlide 0.3s ease;margin:4px 0'; wrap.innerHTML = `
¿Qué tipo de seguro necesitas?
`; chatMessages.appendChild(wrap); scrollToBottom(); return; } if (data.format === 'ta-buttons' || data.format === 'rc-buttons') { addMessage(data.text || data.title, 'bot'); renderGenericButtons(data.buttons, data.lang); return; } if (data.format === 'ta-plan-select') { addMessage(data.text || data.title, 'bot'); renderTaPlanButtons(data.plans, data.lang); return; } if (data.format === 'ta-quote-card') { renderTaQuoteCard(data); window.dataLayer = window.dataLayer || []; // META_CAPI_v1: eventID estable para dedup pixel↔server window.dataLayer.push({ 'event': 'quote_received', eventID: data._capiEventId || null }); } else if(data.format==='rc-quote-card') { if(typeof renderRcQuoteCard==='function') renderRcQuoteCard(data); window.dataLayer = window.dataLayer || []; // META_CAPI_v1: eventID estable para dedup pixel↔server window.dataLayer.push({ 'event': 'quote_received', eventID: data._capiEventId || null }); } else if(data.format==='rc-state-selector') { if(typeof renderRcStateSelector==='function') renderRcStateSelector(data); } else if(data.format==='rc-customize-panel') { if(typeof renderRcCustomizePanel==='function') renderRcCustomizePanel(data); return; } if (data.format === 'ta-customize') { addMessage(data.text || data.title, 'bot'); renderTaCustomizePanel(data); return; } if (data.format === 'versions') { addMessage(`📋 Encontré ${data.versions?.length || 0} versiones (${data.versions?.[0]?.desc?.split(' ')[0] || ''}). Elige la tuya:`, 'bot'); if (data.versions?.length) { renderVersionButtons(data.versions); } return; } // GTM: detect policy issued messages if (data.text && (data.text.includes('Emitida') || data.text.includes('Policy Issued'))) { window.dataLayer = window.dataLayer || []; // Detectar producto y moneda por contexto y extraer monto del texto del bot var isUSD = /USD|US\$|US Dollars/i.test(data.text); var isTourist = location.pathname === '/tourist' || /Tourist|Southbound/i.test(data.text); var isRC = location.pathname === '/rc' || /RC Northbound|Responsabilidad Civil/i.test(data.text); // Regex para sacar el monto: $XX, $XXX.XX, $X,XXX.XX var match = data.text.match(/\$\s*([0-9]{1,3}(?:,[0-9]{3})*(?:\.[0-9]{1,2})?)/); var valueNum = match ? parseFloat(match[1].replace(/,/g, '')) : 0; // META_CAPI_v1: extraer folio (Contract/Contrato/Policy/Póliza ABC-123) del texto para eventID estable var folioMatch = data.text.match(/(?:Contract|Contrato|Policy|P[oó]liza)[^\w]+\*?\*?([A-Za-z0-9\-\_]+)\*?\*?/i); var folioParsed = folioMatch ? folioMatch[1] : null; window.dataLayer.push({ 'event': 'purchase_completed', // META_CAPI_v1: eventID = purchase_ para dedup pixel↔server (mismo formato server-side) eventID: folioParsed ? ('purchase_' + folioParsed) : null, 'value': valueNum, 'currency': isUSD ? 'USD' : 'MXN', 'product': isTourist ? 'tourist_southbound' : isRC ? 'rc_northbound' : 'auto_mx' }); } // Disparar quote_received con value cuando llega cualquier cotización (Tourist/RC) if (data.text && /TA Quote|RC Quote|Quote received|Cotización lista/i.test(data.text)) { window.dataLayer = window.dataLayer || []; var qMatch = data.text.match(/\$\s*([0-9]{1,3}(?:,[0-9]{3})*(?:\.[0-9]{1,2})?)/); var qVal = qMatch ? parseFloat(qMatch[1].replace(/,/g, '')) : 0; var qIsUSD = /USD|US\$/i.test(data.text); var qIsTourist = location.pathname === '/tourist' || /TA Quote|Tourist/i.test(data.text); var qIsRC = location.pathname === '/rc' || /RC Quote|RC Northbound/i.test(data.text); window.dataLayer.push({ 'event': 'quote_received', 'value': qVal, 'currency': qIsUSD ? 'USD' : 'MXN', 'product': qIsTourist ? 'tourist_southbound' : qIsRC ? 'rc_northbound' : 'auto_mx' }); } addMessage(data.text, data.type || 'bot'); // Activate date auto-format when bot asks for a date if (data.type === 'bot' && data.text && /DD\/MM|fecha de nacimiento|date of birth|expiration|expiración|nacimiento.*\(/i.test(data.text)) { dateMode = true; chatInput.setAttribute('inputmode', 'numeric'); chatInput.setAttribute('maxlength', '10'); chatInput.setAttribute('placeholder', 'DD/MM/AAAA'); var calBtnAuto = document.getElementById('calendarBtn'); if (calBtnAuto) calBtnAuto.style.display = 'inline-block'; } else if (data.type === 'bot') { dateMode = false; chatInput.removeAttribute('inputmode'); chatInput.removeAttribute('maxlength'); chatInput.setAttribute('placeholder', currentLang === 'en' ? 'Tell me your car... e.g. Toyota RAV4 2023' : 'Dime tu auto... ej: Toyota RAV4 2023'); var calBtnHide = document.getElementById('calendarBtn'); if (calBtnHide) calBtnHide.style.display = 'none'; } }); socket.on('disconnect', () => { addMessage('Se perdió la conexión. Reconectando...', 'bot'); }); socket.on('reconnect', () => { addMessage('¡Reconectado! 🟢', 'bot'); }); // === TIMI-INPUT-HINT: backend signals date-auto-format mode === socket.on('timi-input-hint', (data) => { const calBtn = document.getElementById('calendarBtn'); const hiddenDate = document.getElementById('hiddenDatePicker'); if (data.inputMode === 'date-auto-format') { dateMode = true; chatInput.setAttribute('inputmode', 'numeric'); chatInput.setAttribute('maxlength', '10'); chatInput.setAttribute('placeholder', data.placeholder || 'DD/MM/AAAA'); if (calBtn) calBtn.style.display = 'inline-block'; // Calendar picker: on date select, format as DD/MM/AAAA if (hiddenDate) { hiddenDate.onchange = function() { if (hiddenDate.value) { const parts = hiddenDate.value.split('-'); // YYYY-MM-DD if (parts.length === 3) { chatInput.value = parts[2] + '/' + parts[1] + '/' + parts[0]; chatInput.focus(); } hiddenDate.value = ''; } }; } } else { dateMode = false; chatInput.removeAttribute('inputmode'); chatInput.removeAttribute('maxlength'); if (calBtn) calBtn.style.display = 'none'; if (data.placeholder) { chatInput.setAttribute('placeholder', data.placeholder); } else { const mode = document.body.dataset.mode || 'auto'; if (mode === 'tourist') { chatInput.setAttribute('placeholder', currentLang === 'en' ? 'Tell me your car... e.g. Toyota RAV4 2023' : 'Dime tu auto... ej: Toyota RAV4 2023'); } else if (mode === 'rc') { chatInput.setAttribute('placeholder', currentLang === 'en' ? 'Tell me your car... e.g. Toyota RAV4 2023' : 'Dime tu auto... ej: Toyota RAV4 2023'); } else { chatInput.setAttribute('placeholder', currentLang === 'en' ? 'Tell me your car... e.g. Toyota RAV4 2023' : 'Dime tu auto... ej: Toyota RAV4 2023'); } } } }); } // ═══ Send Message ═══ function sendMessage() { const text = chatInput.value.trim(); if (!text || !socket) return; addMessage(text, 'user'); socket.emit('user-msg', { text }); chatInput.value = ''; chatInput.focus(); } // ═══ File Upload ═══ // ═══ Compresor client-side de imágenes (2026-04-23) ═══ // Android cámaras sueltan 8-12 MB sin recomprimir (vs iOS 2-4 MB). Algunas caían al límite // de 10 MB o colgaban el FileReader. Aquí bajamos a máx 1920 px / JPEG 0.85 antes del reader. // Si algo falla, fallback al archivo original (no queremos romper al que sí subía bien). async function compressImageIfLarge(file) { try { if (!file.type.startsWith('image/')) return file; if (file.size < 1.5 * 1024 * 1024) return file; // <1.5MB no vale la pena const dataUrl = await new Promise((resolve, reject) => { const r = new FileReader(); r.onload = () => resolve(r.result); r.onerror = reject; r.readAsDataURL(file); }); const img = await new Promise((resolve, reject) => { const im = new Image(); im.onload = () => resolve(im); im.onerror = reject; im.src = dataUrl; }); const MAX = 1920; let w = img.naturalWidth, h = img.naturalHeight; if (w <= MAX && h <= MAX && file.size < 4 * 1024 * 1024) return file; const ratio = Math.min(MAX / w, MAX / h, 1); const cw = Math.round(w * ratio), ch = Math.round(h * ratio); const canvas = document.createElement('canvas'); canvas.width = cw; canvas.height = ch; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, cw, ch); const blob = await new Promise((resolve) => canvas.toBlob(resolve, 'image/jpeg', 0.85)); if (!blob || blob.size >= file.size) return file; // si no redujo, usa original try { console.log('[timi-upload] comprimida', file.name, Math.round(file.size/1024), 'KB →', Math.round(blob.size/1024), 'KB'); } catch(_){} return new File([blob], file.name.replace(/\.(heic|heif|png)$/i,'.jpg'), { type: 'image/jpeg', lastModified: Date.now() }); } catch (e) { try { console.warn('[timi-upload] compresión falló, usando original:', e && e.message); } catch(_){} return file; // fallback silencioso } } async function handleFiles(input) { if (!input.files?.length || !socket) return; const files = Array.from(input.files); // Comprimir imágenes en paralelo antes de iterar (pequeña pausa, pero evita congelar en Android) const processed = await Promise.all(files.map(compressImageIfLarge)); processed.forEach(file => { const maxSize = 10 * 1024 * 1024; // 10MB (post-compresión casi nunca se alcanza) if (file.size > maxSize) { addMessage('La imagen sigue siendo muy grande después de comprimir. Intenta con una foto más chica o menor resolución.', 'bot'); return; } // Show preview const isImage = file.type.startsWith('image/'); const previewDiv = document.createElement('div'); previewDiv.className = 'file-preview-wrap'; if (isImage) { const reader = new FileReader(); reader.onload = (e) => { previewDiv.innerHTML = `
${file.name}
📷 ${file.name}
`; chatMessages.appendChild(previewDiv); scrollToBottom(); }; reader.readAsDataURL(file); } else { previewDiv.innerHTML = `
${file.name}
`; chatMessages.appendChild(previewDiv); scrollToBottom(); } // Show uploading indicator const progressDiv = document.createElement('div'); progressDiv.className = 'upload-progress'; progressDiv.textContent = '📎 Procesando archivo...'; chatMessages.appendChild(progressDiv); scrollToBottom(); // Read file and send as base64 const reader = new FileReader(); reader.onload = (e) => { const base64 = e.target.result.split(',')[1]; socket.emit('user-file', { name: file.name, type: file.type, size: file.size, data: base64 }); progressDiv.textContent = '📎 Archivo enviado, analizando...'; }; reader.readAsDataURL(file); }); // Reset input so same file can be selected again input.value = ''; } // ═══ DATE AUTO-FORMAT: inserts / while typing dates ═══ let dateMode = false; chatInput.addEventListener('input', (e) => { if (!dateMode) return; let val = chatInput.value.replace(/[^\d]/g, ''); // keep only digits if (val.length > 8) val = val.substring(0, 8); if (val.length >= 5) { chatInput.value = val.substring(0,2) + '/' + val.substring(2,4) + '/' + val.substring(4); } else if (val.length >= 3) { chatInput.value = val.substring(0,2) + '/' + val.substring(2); } else { chatInput.value = val; } }); chatInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); dateMode = false; // reset after sending chatInput.removeAttribute('inputmode'); chatInput.removeAttribute('maxlength'); var calBtnReset = document.getElementById('calendarBtn'); if (calBtnReset) calBtnReset.style.display = 'none'; } }); // ═══ Render Messages ═══ // F15 (2026-05-05): autolinker para URLs https:// y rutas /rc /tourist /payment-success. // Antes el bot mandaba HTML inline (...) que se renderizaba pero salía feo en panel admin. // Ahora mandamos texto plano y aquí escapamos + linkificamos URLs visibles. function _escapeHtml(s) { return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } function _autoLinkify(text) { let html = _escapeHtml(text); // URLs https:// y http:// html = html.replace(/(https?:\/\/[^\s<]+)/g, '$1'); // Rutas internas /rc, /tourist, /payment-success, /mx/slug, /usa/slug html = html.replace(/(^|[\s(])(\/(rc|tourist|payment-success|mx\/[a-z0-9-]+|usa\/[a-z0-9-]+))(?=[\s.,!?)]|$)/gi, '$1$2'); return html; } function addMessage(text, type) { if (!text) return; const div = document.createElement('div'); div.className = `msg ${type}`; div.innerHTML = _autoLinkify(text).replace(/\n/g, '
'); chatMessages.appendChild(div); scrollToBottom(); } function showTyping(text) { // Show text message + typing dots if (text) addMessage(text, 'bot'); const div = document.createElement('div'); div.className = 'msg bot typing-msg'; div.id = 'typingIndicator'; div.innerHTML = '
'; chatMessages.appendChild(div); scrollToBottom(); } function removeTyping() { const el = document.getElementById('typingIndicator'); if (el) el.remove(); } // ═══ CIA Brand Colors ═══ const CIA_COLORS = { 'chubb':'#D4A843','qualitas':'#00A551','hdi':'#ED1C24','gnp':'#0066B3', 'axa':'#00008F','zurich':'#1A78C2','aba':'#E87722','sura':'#003DA6', 'mapfre':'#ED1C24','general de seguros':'#1A5276','banorte':'#EB0029', 'afirme':'#003B71','atlas':'#0072BC','primero':'#FF6B00' }; const CIA_LOGOS = { 'chubb':'/img/cias/chubb.png','qualitas':'/img/cias/qualitas.png', 'hdi':'/img/cias/hdi.png','axa':'/img/cias/axa.png','gnp':'/img/cias/gnp.png', 'zurich':'/img/cias/zurich.png','aba':'/img/cias/aba.png', 'sura':'/img/cias/sura.png','mapfre':'/img/cias/mapfre.png' }; function getCiaColor(name) { const n = (name||'').toLowerCase(); for (const [k,v] of Object.entries(CIA_COLORS)) { if (n.includes(k)) return v; } return '#7A8599'; } function getCiaLogo(name) { const n = (name||'').toLowerCase(); for (const [k,v] of Object.entries(CIA_LOGOS)) { if (n.includes(k)) return v; } return null; } // ═══ Quote Cards ═══ function renderQuoteCards(data) { const wrap = document.createElement('div'); wrap.className = 'quote-cards-wrap'; let html = `
Respaldados por Chubb Quálitas
🚗 ${data.vehiculo} ${data.paquete}
📅 Modelo ${data.anio || ''} 🏷 ${data.paquete} 📍 ${data.cp || '44690'} 👤 ${data.edad || 30} años ${data.genero || ''}
`; data.quotes.forEach((q, i) => { const medals = ['🥇','🥈','🥉']; const medal = medals[i] || `${i+1}`; // Build coberturas detail HTML let detailHtml = ''; if (q.coberturas && q.coberturas.length) { detailHtml = q.coberturas.map(c => { const nombre = c.nombre || c.Nombre || c.descripcion || ''; const sa = c.sumaAsegurada || c.SumaAsegurada || c.sa || ''; const ded = c.deducible || c.Deducible || ''; return `
${nombre}${sa}${ded ? ' / Ded: '+ded : ''}
`; }).join(''); } const ciaLogo = getCiaLogo(q.cia); const logoRight = ciaLogo ? `` : ''; html += `
${medal} ${q.cia} ${q.isBest ? 'MEJOR' : ''}
${q.totalFormatted}
Neta: ${q.neta}
DM: ${q.dm} · RT: ${q.rt} ${q.numCoberturas ? ` · ${q.numCoberturas} cob.` : ''}
${detailHtml ? `
Coberturas incluidas
${detailHtml}
` : '
Coberturas no disponibles en detalle
'}
${logoRight}
`; }); html += `
`; // Footer con mejor precio y ahorro html += ` `; wrap.innerHTML = html; chatMessages.appendChild(wrap); scrollToBottom(); } function toggleQcard(el) { // Toggle expanded state to show/hide coberturas const wasExpanded = el.classList.contains('expanded'); // Close all others first el.closest('.quote-grid').querySelectorAll('.qcard.expanded').forEach(c => c.classList.remove('expanded')); if (!wasExpanded) el.classList.add('expanded'); } function contratar(cia, precio) { if (!socket) return; addMessage(`¡Quiero contratar! Me interesa ${cia} (${precio})`, 'user'); socket.emit('user-msg', { text: `CONTRATAR:${cia}` }); setChatStep('contratar'); // STEP_CHIPS_v1: avanza al último paso } // ═══ Closing Card (CLOSING_CARD_v1, 2026-05-06): refuerzo visual post-cotización ═══ function renderClosingCard(d) { const wrap = document.createElement('div'); wrap.className = 'closing-card-wrap'; const lang = d.lang || 'es'; const isEn = lang === 'en'; const bullets = (d.beneficios || []).map(b => `
  • ${b}
  • `).join(''); const wallet = (d.wallet_features || []).map(w => `
    ${w.icon || '✨'}
    ${w.title || ''}
    ${w.desc || ''}
    `).join(''); const sp = d.social_proof_count > 0 ? `` : ''; const ctaText = d.cta_text || (isEn ? `🛡️ Lock my ${d.cia || ''} policy` : `🛡️ ¡Quiero contratar con ${d.cia || ''}!`); const safeCia = (d.cia || '').replace(/'/g, ''); const safeTotal = (d.total || '').replace(/'/g, ''); // COMPARISON_v1 (2026-05-08, Michelle #3): mini-tabla de opciones cotizadas, ganadora destacada const compRows = Array.isArray(d.comparativo) && d.comparativo.length >= 2 ? d.comparativo.map(c => `
    ${c.isBest ? '🥇 ' : ''}${c.cia} ${c.total}${c.isBest ? (isEn ? ' ✓ best' : ' ✓ mejor') : ''}
    `).join('') : ''; const compSection = compRows ? `
    📊 ${isEn ? 'Why this is your best option' : 'Por qué es tu mejor opción'}
    ${compRows}
    ` : ''; const usdHint = d.total_usd_hint ? `
    ${isEn ? '≈' : 'aprox.'} ${d.total_usd_hint}
    ` : ''; wrap.innerHTML = `
    🚗 ${d.vehiculo || ''}
    ${d.total || ''}${isEn ? '/year' : '/año'}
    ${usdHint} ${d.mensualidad ? `
    ${isEn ? 'or from' : 'o desde'} ${d.mensualidad}/${isEn ? 'mo' : 'mes'} ${isEn ? '× 12 months' : 'a 12 mensualidades'} 💳
    ` : ''}
    ${isEn ? 'with' : 'con'} ${d.cia || ''}
    ${compSection} ${bullets ? `
    🛡️ ${isEn ? 'Your car is protected' : 'Tu auto queda protegido'}
      ${bullets}
    ` : ''} ${wallet ? `
    🎁 ${isEn ? 'Plus, you get' : 'Y al contratar hoy obtienes'} ${isEn ? 'your Timi Wallet' : 'tu Wallet Timi'}
    ${wallet}
    ` : ''} ${sp}
    🔒 ${isEn ? 'Stripe secure payment' : 'Pago seguro Stripe'} · 📄 ${isEn ? 'Policy at issuance' : 'Póliza al instante'} · ⏰ ${isEn ? 'Quote valid 24h' : 'Cotización válida 24h'}
    `; chatMessages.appendChild(wrap); scrollToBottom(); } // ═══ Factura Card ═══ function renderFacturaCard(d) { const wrap = document.createElement('div'); wrap.className = 'doc-card'; const warningHtml = (d.diasFactura && d.diasFactura > 30) ? `
    ⚠️ Factura emitida hace ${d.diasFactura} días (más de 30). Se cotizará a valor comercial.
    ` : ''; wrap.innerHTML = `
    ✅ Datos extraídos de la factura FACTURA
    🚗 ${d.descFactura || (d.marca+' '+d.modelo)} ${d.transmision ? '· '+d.transmision : ''}
    Modelo
    ${d.anio || '—'}
    Transmisión
    ${d.transmision || 'N/D'}
    ${d.fechaEmision ? `
    Fecha emisión
    ${d.fechaEmision}${d.diasFactura ? ' ('+d.diasFactura+' días)' : ''}
    ` : ''}
    Cotiza a
    ${d.cotizaA}${d.valorFactura && d.valorFacturaAplica ? ' · '+d.valorFactura : ''}
    ${d.cp ? `
    📍 CP
    ${d.cp}
    ` : ''} ${d.edad ? `
    👤 Edad
    ${d.edad} años
    ` : ''} ${d.genero ? `
    Género
    ${d.genero}
    ` : ''} ${d.cliente ? `
    👤 Cliente
    ${d.cliente}
    ` : ''} ${d.rfc ? `
    📋 RFC
    ${d.rfc}
    ` : ''} ${d.tipoPersona ? `
    Tipo
    ${d.tipoPersona === 'fisica' ? 'Persona Física' : 'Persona Moral'}
    ` : ''} ${d.niv ? `
    🔑 NIV
    ${d.niv}
    ` : ''} ${d.motor ? `
    Motor
    ${d.motor}
    ` : ''} ${d.color ? `
    Color
    ${d.color}
    ` : ''}
    ${warningHtml}
    `; chatMessages.appendChild(wrap); scrollToBottom(); } // ═══ Contratación Card ═══ function renderContratacionCard(d) { const wrap = document.createElement('div'); wrap.className = 'doc-card'; wrap.innerHTML = `
    ✅ Datos para contratación LISTO
    🛡️ Contratar con ${d.cia}
    👤 Contratante
    ${d.nombre}
    ${d.rfc && d.rfc !== 'No proporcionado' ? `
    📋 RFC
    ${d.rfc}
    ` : ''}
    🚗 Vehículo
    ${d.vehiculo} ${d.anio || ''}
    ${d.niv ? `
    🔑 NIV
    ${d.niv}
    ` : ''} ${d.domicilio ? `
    📍 Domicilio
    ${d.domicilio}
    ` : ''}
    📧 Email
    ${d.email}
    📱 Celular
    ${d.celular}
    `; chatMessages.appendChild(wrap); scrollToBottom(); } // ═══ Version Buttons ═══ function renderVersionButtons(versions) { const wrap = document.createElement('div'); wrap.className = 'version-buttons'; versions.forEach(v => { const btn = document.createElement('button'); btn.className = 'version-btn'; btn.innerHTML = `${v.idx}. ${v.desc}`; btn.onclick = () => { addMessage(String(v.idx), 'user'); socket.emit('user-msg', { text: String(v.idx) }); // Disable all version buttons wrap.querySelectorAll('.version-btn').forEach(b => { b.disabled = true; b.style.opacity = '0.5'; }); }; wrap.appendChild(btn); }); chatMessages.appendChild(wrap); scrollToBottom(); } function renderGenericButtons(buttons, lang) { const wrap = document.createElement('div'); wrap.className = 'version-buttons'; wrap.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;padding:8px 0'; buttons.forEach(b => { const btn = document.createElement('button'); btn.className = 'version-btn'; btn.style.cssText = 'padding:10px 16px;font-size:13px;min-width:auto;white-space:normal;text-align:center'; btn.textContent = b.label || b; btn.onclick = () => { let val = b.value || b.label || b; const lbl = b.label || b; // If "Today/Hoy" button → append client local time so server can filter hour options if (b.id === 'today' || val === '__RC_START_TODAY__' || lbl === 'Hoy' || lbl === 'Today') { const now = new Date(); const hh = String(now.getHours()).padStart(2, '0'); const mm = String(now.getMinutes()).padStart(2, '0'); val = val + '|' + hh + ':' + mm; } addMessage(lbl, 'user'); socket.emit('user-msg', { text: val }); wrap.querySelectorAll('.version-btn').forEach(x => { x.disabled = true; x.style.opacity = '0.5'; }); }; wrap.appendChild(btn); }); chatMessages.appendChild(wrap); scrollToBottom(); } function scrollToBottom() { requestAnimationFrame(() => { chatMessages.scrollTop = chatMessages.scrollHeight; }); } // ═══ Navbar scroll ═══ window.addEventListener('scroll', () => { navbar.classList.toggle('scrolled', window.scrollY > 40); }); // ═══ Chat Demo Animation (Hero mockup) ═══ const chatTimeline = [ { id: 'msg1', delay: 600 }, { id: 'msg2', delay: 2000 }, { id: 'typing', delay: 1000 }, { id: 'msg3', delay: 2200, hideTyping: true }, { id: 'msgResult', delay: 1800 }, ]; let chatIndex = 0; function runChat() { if (chatIndex >= chatTimeline.length) { setTimeout(() => { chatTimeline.forEach(item => { const el = document.getElementById(item.id); if (el) el.classList.remove('visible'); }); chatIndex = 0; setTimeout(runChat, 800); }, 6000); return; } const step = chatTimeline[chatIndex]; setTimeout(() => { if (step.hideTyping) document.getElementById('typing').classList.remove('visible'); const el = document.getElementById(step.id); if (el) el.classList.add('visible'); chatIndex++; runChat(); }, step.delay); } runChat(); // ═══ Scroll Reveal ═══ const revealEls = document.querySelectorAll('.reveal'); const observer = new IntersectionObserver((entries) => { entries.forEach((entry, i) => { if (entry.isIntersecting) { setTimeout(() => entry.target.classList.add('visible'), i * 100); observer.unobserve(entry.target); } }); }, { threshold: 0.15 }); revealEls.forEach(el => observer.observe(el)); // ═══ Check URL hash/param for auto-open ═══ if (window.location.hash === '#cotiza' || window.location.pathname === '/cotiza' || new URLSearchParams(window.location.search).get('chat') === '1') { setTimeout(openChat, 500); } // ═══ Drag & Drop support ═══ const dropZone = document.getElementById('chatMessages'); if (dropZone) { ['dragenter', 'dragover'].forEach(evt => { dropZone.addEventListener(evt, (e) => { e.preventDefault(); e.stopPropagation(); dropZone.style.border = '2px dashed var(--coral)'; dropZone.style.background = 'rgba(255,107,90,0.05)'; }); }); ['dragleave', 'drop'].forEach(evt => { dropZone.addEventListener(evt, (e) => { e.preventDefault(); e.stopPropagation(); dropZone.style.border = ''; dropZone.style.background = ''; }); }); dropZone.addEventListener('drop', (e) => { const files = e.dataTransfer?.files; if (files?.length) { // Reuse the existing file handler const fileInput = document.getElementById('fileInput'); const dt = new DataTransfer(); for (const f of files) dt.items.add(f); fileInput.files = dt.files; handleFiles(fileInput); } }); }